package daybook.io.formatter;

import daybook.model.Account;
import daybook.model.Book;
import daybook.model.Commodity;
import daybook.model.transaction.*;
import daybook.util.Pair;
import daybook.util.StreamUtils;
import daybook.util.tree.Tree;
import org.json.JSONArray;
import org.json.JSONObject;

import java.io.Writer;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static daybook.model.transaction.TransactionSummary.amountsCollector;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.reducing;

public class ToJson {
    public static final int INDENT_FACTOR = 3;

    public static JSONObject statistics(TransactionSummaryStatistics statistics) {
        return new JSONObject(statistics);
    }

    public static void writeStatistics(TransactionSummaryStatistics statistics, Writer writer) {
        statistics(statistics).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONArray transactions(Stream<Transaction> transactions) {
        return transactions.map(ToJson::transaction)
                .collect(StreamUtils.jsonArrayCollector());
    }

    public static void writeTransactions(Stream<Transaction> transactions, Writer writer) {
        transactions(transactions).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONObject ledger(Map<Account, List<LedgerEntry>> ledger) {
        final JSONObject json = new JSONObject();
        for (Map.Entry<Account, List<LedgerEntry>> e: ledger.entrySet()) {
            json.put(e.getKey().getCode(),
                    e.getValue().stream()
                            .map(ToJson::ledgerEntry)
                            .collect(StreamUtils.jsonArrayCollector()));
        }
        return json;
    }

    public static void writeLedger(Map<Account, List<LedgerEntry>> ledger, Writer writer) {
        ledger(ledger).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONObject balanceTreeToJson(Tree<Pair<Account, BigDecimal>> balanceTree) {
        if (balanceTree.isLeaf()) {
            final JSONObject json = new JSONObject();
            json.put(balanceTree.getValue().first().getCode(),
                    balanceTree.getValue().second().toPlainString());
            return json;
        } else {
            final JSONObject json = new JSONObject();
            json.put(balanceTree.getValue().first().getCode(),
                    balanceTree.getValue().second().toPlainString());
            json.put("children",
                    balanceTree.getChildren().stream()
                            .map(ToJson::balanceTreeToJson)
                            .collect(Collectors.toList()));
            return json;
        }
    }

    public static JSONArray balanceSheet(
            NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> balanceSheets,
            LocalDate startDate) {
        final JSONArray array = new JSONArray();
        LocalDate lastDate = startDate;
        for (Map.Entry<LocalDate, Tree<Pair<Account, BigDecimal>>> entry: balanceSheets.entrySet()) {
            final JSONObject json = new JSONObject();
            final Tree<Pair<Account, BigDecimal>> balanceTree = entry.getValue();
            json.put("startDate", lastDate)
                    .put("endDate", entry.getKey().plusDays(-1L));
            if (balanceTree != null) {
                json.put("balanceTree", balanceTreeToJson(balanceTree));
                final Map<Account.Type, BigDecimal> typeToAmount =
                        balanceTree.getChildren().stream()
                                .map(Tree::getValue)
                                .collect(groupingBy(pair -> pair.first().getType(),
                                        reducing(BigDecimal.ZERO, Pair::second, BigDecimal::add)));
                for (Account.Type type: Account.Type.values()) {
                    typeToAmount.putIfAbsent(type, BigDecimal.ZERO);
                }
                final BigDecimal totalEquity = typeToAmount.get(Account.Type.EQUITY)
                        .add(typeToAmount.get(Account.Type.REVENUE))
                        .add(typeToAmount.get(Account.Type.EXPENSE));
                final Map<String, String> balances = typeToAmount.entrySet().stream()
                        .collect(Collectors.toMap(e -> e.getKey().name().toLowerCase(), e -> e.getValue().toPlainString()));
                balances.put("totalEquity", totalEquity.toPlainString());
                json.put("balances", balances);
            }
            array.put(json);
            lastDate = entry.getKey();
        }
        return array;
    }

    public static void writeBalanceSheet(
            NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> balanceSheets,
            LocalDate startDate, Writer writer) {
        balanceSheet(balanceSheets, startDate).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONArray incomeStatement(
            NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> incomeStatements,
            LocalDate startDate) {
        final JSONArray array = new JSONArray();
        LocalDate lastDate = startDate;
        for (Map.Entry<LocalDate, Tree<Pair<Account, BigDecimal>>> entry: incomeStatements.entrySet()) {
            final JSONObject json = new JSONObject();
            final Tree<Pair<Account, BigDecimal>> balanceTree = entry.getValue();
            json.put("startDate", lastDate)
                    .put("endDate", entry.getKey().plusDays(-1L));
            if (balanceTree != null) {
                json.put("balanceTree", balanceTreeToJson(balanceTree));
                final Map<Account.Type, BigDecimal> typeToAmount =
                        balanceTree.getChildren().stream()
                                .map(Tree::getValue)
                                .collect(groupingBy(pair -> pair.first().getType(),
                                        reducing(BigDecimal.ZERO, Pair::second, BigDecimal::add)));
                typeToAmount.putIfAbsent(Account.Type.REVENUE, BigDecimal.ZERO);
                typeToAmount.putIfAbsent(Account.Type.EXPENSE, BigDecimal.ZERO);
                final BigDecimal profitOrLoss = typeToAmount.get(Account.Type.REVENUE)
                        .add(typeToAmount.get(Account.Type.EXPENSE))
                        .negate();
                final Map<String, String> balances = typeToAmount.entrySet().stream()
                        .collect(Collectors.toMap(e -> e.getKey().name().toLowerCase(), e -> e.getValue().toPlainString()));
                balances.put("profitOrLoss", profitOrLoss.toPlainString());
                json.put("balances", balances);
            }
            array.put(json);
            lastDate = entry.getKey();
        }
        return array;
    }

    public static void writeIncomeStatement(
            NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> incomeStatements,
            LocalDate startDate, Writer writer) {
        incomeStatement(incomeStatements, startDate).write(writer, INDENT_FACTOR, 0);
    }

    private static JSONObject flowToJson(Map<Account, BigDecimal[]> flow, Set<Account> accounts) {
        final JSONObject json = new JSONObject();
        for (Map.Entry<Account, BigDecimal[]> entry: flow.entrySet()) {
            final List<String> amounts = Arrays.stream(entry.getValue())
                    .map(BigDecimal::toPlainString)
                    .collect(Collectors.toList());
            json.put(entry.getKey().getCode(), amounts);
        }

        if (!accounts.isEmpty()) {
            final List<String> amounts = Arrays.asList("0", "0");
            for (Account account: accounts) {
                if (!flow.containsKey(account)) {
                    json.put(account.getCode(), amounts);
                }
            }
        }

        return json;
    }

    private static JSONObject flowToSummary(Map<Account, BigDecimal[]> flow) {
        final BigDecimal[] amounts = flow.entrySet().stream()
                .map(Map.Entry::getValue)
                .collect(amountsCollector());
        final JSONObject json = new JSONObject();
        json.put("debit", amounts[0].toPlainString())
                .put("credit", amounts[1].toPlainString())
                .put("sum", amounts[0].add(amounts[1]).toPlainString());
        return json;
    }

    public static JSONArray cashFlow(
            NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlows,
            LocalDate startDate, Set<Account> cashAccounts) {
        final JSONArray array = new JSONArray();
        LocalDate lastDate = startDate;
        for (Map.Entry<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> entry: cashFlows.entrySet()) {
            final JSONObject json = new JSONObject();
            json.put("startDate", lastDate)
                    .put("endDate", entry.getKey().plusDays(-1L));
            final Map<Account, BigDecimal[]> cash = entry.getValue().get(true);
            json.put("cash", flowToJson(cash, cashAccounts));
            json.put("cashSummary", flowToSummary(cash));
            final Map<Account, BigDecimal[]> other = entry.getValue().get(false);
            json.put("other", flowToJson(other, Collections.emptySet()));
            json.put("otherSummary", flowToSummary(cash));
            array.put(json);
            lastDate = entry.getKey();
        }
        return array;
    }

    public static void writeCashFlow(
            NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlows,
            LocalDate startDate, Set<Account> cashAccounts, Writer writer) {
        cashFlow(cashFlows, startDate, cashAccounts).write(writer, INDENT_FACTOR, 0);
    }

    public static void writeBook(Book book, Writer writer) {
        book(book).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONObject inventory(Map<Commodity, List<InventoryEntry>> inventory) {
        final JSONObject json = new JSONObject();
        for (Map.Entry<Commodity, List<InventoryEntry>> entry: inventory.entrySet()) {
            json.put(entry.getKey().getName(), entry.getValue().stream()
                    .map(ToJson::inventoryEntry)
                    .collect(Collectors.toList()));
        }
        return json;
    }

    public static void writeInventory(Map<Commodity, List<InventoryEntry>> inventory, Writer writer) {
        inventory(inventory).write(writer, INDENT_FACTOR, 0);
    }

    public static void writeCollection(Collection<?> collection, Writer writer) {
        new JSONArray(collection).write(writer, INDENT_FACTOR, 0);
    }

    public static JSONObject account(Account account) {
        final JSONObject json = new JSONObject();
        json.put("code", account.getCode())
                .put("name", account.getName())
                .put("description", account.getDescription())
                .put("attributes", account.getAttributes().stream().collect(Collectors.toList()))
                .put("aliases", account.getAliases().stream().collect(Collectors.toList()));
        account.getCommodity().ifPresent(commodity -> {
            json.put("commodity", commodity.getName());
        });
        if (account.getParentCode().equals(Account.ROOT.getCode())) {
            json.put("type", account.getType().name().toLowerCase());
        } else {
            json.put("parent", account.getParentCode());
        }

        return json;
    }

    public static JSONObject commodity(Commodity commodity) {
        return new JSONObject(commodity);
    }

    public static JSONObject ledgerEntry(LedgerEntry ledgerEntry) {
        final JSONObject json = new JSONObject();
        json.put("date", ledgerEntry.getDate())
                .put("transactionOrder", ledgerEntry.getTransaction().getOrder())
                .put("checked", ledgerEntry.isChecked())
                .put("account", ledgerEntry.getAccount().getCode())
                .put("amount", ledgerEntry.getAmount().toPlainString())
                .put("description", ledgerEntry.getDescription())
                .put("balance", ledgerEntry.getBalance().toPlainString())
                .put("itemDescription", ledgerEntry.getTransactionEntry().getDescription());
        if (!ledgerEntry.getItems().isEmpty()) {
            json.put("items", ledgerEntry.getItems().stream()
                    .map(ToJson::item)
                    .collect(Collectors.toList()));
        }

        return json;
    }

    public static JSONObject transaction(Transaction transaction) {
        final JSONObject json = new JSONObject();
        json.put("date", transaction.getDate())
                .put("description", transaction.getDescription())
                .put("order", transaction.getOrder())
                .put("checked", transaction.isChecked())
                .put("entries", transaction.getEntries().stream().map(ToJson::transactionEntry).collect(Collectors.toList()));
        return json;
    }

    public static JSONObject transactionEntry(TransactionEntry transactionEntry) {
        final JSONObject json = new JSONObject();
        json.put("account", transactionEntry.getAccount().getCode())
                .put("amount", transactionEntry.getAmount().toPlainString())
                .put("description", transactionEntry.getDescription())
                .put("tags", transactionEntry.getTags());
        if (!transactionEntry.getItems().isEmpty()) {
            json.put("items", transactionEntry.getItems().stream()
                    .map(ToJson::item)
                    .collect(Collectors.toList()));
        }

        return json;
    }

    public static JSONObject item(Item item) {
        final JSONObject json = new JSONObject();
        json.put("count", item.getCount().toPlainString())
                .put("cost", item.getCost().toPlainString());
        return json;
    }

    public static JSONObject inventoryEntry(InventoryEntry inventoryEntry) {
        final JSONObject json = new JSONObject();
        final Map<String, String> costBalance = inventoryEntry.getCostBalance().entrySet().stream()
                .collect(Collectors.toMap(e -> e.getKey().toPlainString(), e -> e.getValue().toPlainString()));
        json.put("date", inventoryEntry.getDate())
                .put("transactionOrder", inventoryEntry.getTransaction().getOrder())
                .put("amount", inventoryEntry.getAmount().toPlainString())
                .put("balance", inventoryEntry.getBalance().toPlainString())
                .put("countBalance", inventoryEntry.getCountBalance().toPlainString())
                .put("costBalance", costBalance);
        if (!inventoryEntry.getItems().isEmpty()) {
            json.put("items", inventoryEntry.getItems().stream()
                    .map(ToJson::item)
                    .collect(Collectors.toList()));
        }
        inventoryEntry.getAverageCost().ifPresent(averageCost -> json.put("averageCost", averageCost));

        return json;
    }

    public static JSONObject book(Book book) {
        final JSONObject json = new JSONObject();
        json.put("accountNameSeparator", book.getAccountNameSeparator())
                .put("defaultCurrency", book.getDefaultCurrency().getName())
                .put("commodities", book.getCommodities().values())
                .put("accounts", book.getChartOfAccounts().getTree().getChildren().stream()
                        .flatMap(Tree::stream)
                        .map(ToJson::account)
                        .collect(Collectors.toList()));

        return json;
    }
}
